package br.edu.ufcg.ga;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

import org.jgap.Chromosome;
import org.jgap.Configuration;
import org.jgap.DefaultFitnessEvaluator;
import org.jgap.FitnessFunction;
import org.jgap.Gene;
import org.jgap.Genotype;
import org.jgap.IChromosome;
import org.jgap.InvalidConfigurationException;
import org.jgap.NaturalSelectorExt;
import org.jgap.Population;
import org.jgap.event.EventManager;
import org.jgap.impl.BestChromosomesSelector;
import org.jgap.impl.ChromosomePool;
import org.jgap.impl.GreedyCrossover;
import org.jgap.impl.IntegerGene;
import org.jgap.impl.SwappingMutationOperator;
import org.jgap.impl.TournamentSelector;
import org.jgap.impl.WeightedRouletteSelector;
import org.jgap.impl.salesman.Salesman;

import br.edu.ufcg.ga.output.FullOutputGenerator;
import br.edu.ufcg.ga.output.FullOutputInfo;
import br.edu.ufcg.ga.output.MainRunOutputGenerator;
import br.edu.ufcg.ga.output.MainRunOutputInfo;
import br.edu.ufcg.ga.output.OutputGenerator;
import br.edu.ufcg.ga.output.OutputInfo;
import br.edu.ufcg.ga.parser.args.ArgsParser;
import br.edu.ufcg.ga.population.InitialPopulationGenerator;
import br.edu.ufcg.ga.random.SeedRandomGenerator;
import br.edu.ufcg.ga.stopcondition.HybridStopCondition;
import br.edu.ufcg.ga.stopcondition.OptimalStopCondition;
import br.edu.ufcg.ga.stopcondition.StopCondition;

public class CaixeiroViajante extends Salesman {

	private static final int[][] DEFAULT_DISTANCES = {
			{ 0, 42, 61, 30, 17, 82, 31, 11 },
			{ 42, 0, 14, 87, 28, 70, 19, 33 },
			{ 61, 14, 0, 20, 81, 21, 8, 29 },
			{ 30, 87, 20, 0, 34, 33, 91, 10 },
			{ 17, 28, 81, 34, 0, 41, 34, 82 },
			{ 82, 70, 21, 33, 41, 0, 19, 32 },
			{ 31, 19, 8, 91, 34, 19, 0, 59 }, { 11, 33, 29, 10, 82, 32, 59, 0 } };
	private static final double DELTA = 0.00001d;

	public static enum NaturalSelectorItem {
		BEST, ROULETTE, TOURNAMENT
	};

	private int[][] distanceMatrix = DEFAULT_DISTANCES;
	private Configuration m_config;

	private long initTime;

	private long endTime;

	private boolean preserveFittestIndividual;

	private double selectorRate;

	private NaturalSelectorItem naturalSelectorItem;

	private boolean doubletteChromosomesAllowed;

	private int tournamentSize;

	private StopCondition stopCondition;

	private SeedRandomGenerator random;

	private OutputGenerator outputGenerator;

	private OutputInfo outputInfo;
	
	private InitialPopulationGenerator initialPopulationGenerator;
	
	public CaixeiroViajante(int[][] distanceMatrix,
			boolean preserveFittestIndividual, double selectorRate,
			NaturalSelectorItem naturalSelectorItem,
			boolean doubletteChromosomesAllowed, int tournamentSize,
			StopCondition stopCondition) {

		this.tournamentSize = tournamentSize;
		this.initTime = System.currentTimeMillis();
		this.distanceMatrix = distanceMatrix;
		this.selectorRate = selectorRate;
		this.naturalSelectorItem = naturalSelectorItem;
		this.preserveFittestIndividual = preserveFittestIndividual;
		this.doubletteChromosomesAllowed = doubletteChromosomesAllowed;
		this.stopCondition = stopCondition;
		this.random = new SeedRandomGenerator();
		this.outputGenerator = new FullOutputGenerator();
		this.outputInfo = new FullOutputInfo();
	}

	public CaixeiroViajante(boolean preserveFittestIndividual,
			double selectorRate, NaturalSelectorItem naturalSelectorItem,
			boolean doubletteChromosomesAllowed, int tournamentSize,
			StopCondition stopCondition) {
		this(DEFAULT_DISTANCES, preserveFittestIndividual, selectorRate,
				naturalSelectorItem, doubletteChromosomesAllowed,
				tournamentSize, stopCondition);
	}

	// public CaixeiroViajante(String inputFile) throws FileNotFoundException {
	// this(buildDistanceMatrix(new File(inputFile)));
	// }

	public CaixeiroViajante(CVConfiguration configuration) throws InvalidConfigurationException {
		this.m_config = configuration.getConfiguration();
		this.initTime = System.currentTimeMillis();
		
		this.random = new SeedRandomGenerator(configuration.getSeed());
		
		Double optimalValueStopCondition = configuration.getOptimalValueStopCondition();
		if(optimalValueStopCondition == null){
			this.stopCondition = new HybridStopCondition(configuration.getMaxGenerations(), configuration.getMaxRepetitionBestFitness());
		} else {
			this.stopCondition = new OptimalStopCondition(optimalValueStopCondition);
		}
		
		if(configuration.isCollectData()){
			this.outputGenerator = new FullOutputGenerator();
			this.outputInfo = new FullOutputInfo();
		} else {
			this.outputGenerator = new MainRunOutputGenerator();
			this.outputInfo = new MainRunOutputInfo();
		}

		this.initialPopulationGenerator = configuration.getInitialPopulationGenerator();
		
		this.m_config.setRandomGenerator(this.random);
		this.m_config.setMinimumPopSizePercent(0);
		this.m_config.setEventManager(new EventManager());
		this.m_config.setFitnessEvaluator(new DefaultFitnessEvaluator());
		this.m_config.setChromosomePool(new ChromosomePool());
	}

	public int[][] getDistanceMatrix() {
		return distanceMatrix;
	}

	private static int[][] buildDistanceMatrix(File inputFile)
			throws FileNotFoundException {
		Scanner sc = new Scanner(inputFile);

		int nCities;
		int[][] distances = null;

		if (sc.hasNextLine()) {
			String[] firstDists = sc.nextLine().split(" ");
			nCities = firstDists.length;
			distances = new int[nCities][nCities];

			for (int i = 0; i < nCities; i++) {
				distances[0][i] = Integer.valueOf(firstDists[i]);
			}

			int currentLine = 1;

			while (sc.hasNextLine()) {

				String[] line = sc.nextLine().split("\\s+");

				for (int i = 0; i < line.length; i++) {
					distances[currentLine][i] = Integer.valueOf(line[i]);
				}
				currentLine++;
			}
		}
		sc.close();

		return distances;
	}

	public IChromosome createSampleChromosome(Object a_initial_data) {
		try {
			int nCities = distanceMatrix.length;
			Gene[] genes = new Gene[nCities];
			for (int i = 0; i < genes.length; i++) {
				genes[i] = new IntegerGene(getConfiguration(), 0, nCities - 1);
				genes[i].setAllele(i);
			}
			return new Chromosome(getConfiguration(), genes);
		} catch (InvalidConfigurationException iex) {
			throw new IllegalStateException(iex.getMessage());
		}
	}

	public Configuration createConfiguration(Object a_initial_data)
			throws InvalidConfigurationException {
		// This is copied from DefaultConfiguration.
		// -----------------------------------------
		Configuration config = new Configuration();

		NaturalSelectorExt naturalSelector = null;

		switch (naturalSelectorItem) {
		case BEST:
			naturalSelector = new BestChromosomesSelector(config, selectorRate);
			break;
		case ROULETTE:
			naturalSelector = new WeightedRouletteSelector(config);
			break;
		case TOURNAMENT:
			naturalSelector = new TournamentSelector(config, tournamentSize,
					selectorRate);
			break;
		default:
			naturalSelector = new BestChromosomesSelector(config, selectorRate);
			break;
		}

		naturalSelector
				.setDoubletteChromosomesAllowed(doubletteChromosomesAllowed);

		config.addNaturalSelector(naturalSelector, true);
		config.setRandomGenerator(this.random);
		config.setMinimumPopSizePercent(0);
		config.setEventManager(new EventManager());
		config.setFitnessEvaluator(new DefaultFitnessEvaluator());
		config.setChromosomePool(new ChromosomePool());
		// These are different:
		// --------------------
		config.addGeneticOperator(new GreedyCrossover(config));
		config.addGeneticOperator(new SwappingMutationOperator(config, 20));
		return config;
	}

	public double distance(Gene fromCity, Gene toCity) {
		return distanceMatrix[((IntegerGene) fromCity).intValue()][((IntegerGene) toCity)
				.intValue()];
	}

	public IChromosome findOptimalPath(Object a_initial_data) throws Exception {
		if (m_config == null) {
			m_config = createConfiguration(a_initial_data);
		}

		FitnessFunction myFunc = createFitnessFunction(null);
		m_config.setFitnessFunction(myFunc);
		// Now we need to tell the Configuration object how we want our
		// Chromosomes to be setup. We do that by actually creating a
		// sample Chromosome and then setting it on the Configuration
		// object.
		// --------------------------------------------------------------
		IChromosome sampleChromosome = createSampleChromosome(a_initial_data);

		m_config.setSampleChromosome(sampleChromosome);

		// Create random initial population of Chromosomes.
		// ------------------------------------------------

		// As we cannot allow the normal mutations if this task,
		// we need multiple calls to createSampleChromosome.
		// -----------------------------------------------------
		IChromosome[] chromosomes = this.initialPopulationGenerator.createSampleChromosomes(sampleChromosome, m_config);

		// Create the genotype. We cannot use Genotype.randomInitialGenotype,
		// Because we need unique gene values (representing the indices of the
		// cities of our problem).
		// -------------------------------------------------------------------
		Genotype population = new Genotype(m_config, new Population(m_config,
				chromosomes));
		IChromosome best = null;
		// Evolve the population. Since we don't know what the best answer
		// is going to be, we just evolve the max number of times.
		// ---------------------------------------------------------------
		double bestFitnessValue = -1;

		int generation = 0;
		do {
			generation++;
			population.evolve();
			best = population.getFittestChromosome();
			bestFitnessValue = Integer.MAX_VALUE / 2 - best.getFitnessValue();
			this.outputInfo.add(bestFitnessValue);
		} while (!this.stopCondition.reached(generation, bestFitnessValue));

		this.endTime = System.currentTimeMillis();

		// Return the best solution we found.
		// ----------------------------------
		return best;
	}

	public Configuration getConfiguration() {
		return m_config;
	}

	public SeedRandomGenerator getRandom() {
		return random;
	}

	public StopCondition getStopCondition() {
		return stopCondition;
	}

	public long getInitTime() {
		return initTime;
	}

	public long getEndTime() {
		return endTime;
	}

	public OutputInfo getOutputInfo() {
		return outputInfo;
	}

	public void setConfig(Configuration config) {
		m_config = config;
	}

	public static void main(String[] args) {

		ArgsParser ap = new ArgsParser();
		ap.init();
		
		if(args.length == 1 && (args[0].equals("--help") || args[0].equals("-h") )){
			ap.printUsage();
			System.exit(0);
		}
		
		if(args.length == 0){
			args = "-SO_PS=512 -SO_MG=6 -SO_MRBF=5 -SO_CD=false -NS_TS -TS_SR=0.9 -TS_TS=3 -GO_GCO -GO_SMO -SMO_MR=0.05".replaceAll("=", " ").split(" ");
		}
		
		CVConfiguration configuration = null;
		CaixeiroViajante cv = null;

		try {
			configuration = ap.parseArgs(args);
			cv = new CaixeiroViajante(configuration); 
			//		new OptimalStopCondition(140.0));
			// new HybridStopCondition(6, 5));
			// new MaxGenerationsStopCondition(10));
			// new MaxBestFitnessRepetitionStopCondition(5));
		} catch (Exception e) {
			System.err.println();
			System.err.println(">>> " + e.getMessage());
			System.err.println();
			ap.printUsage();
			System.exit(1);
		}

		try {
			IChromosome optimal = cv.findOptimalPath(null);
			double bestFitnessValue = Integer.MAX_VALUE / 2
					- optimal.getFitnessValue();
			cv.outputGenerator.generateOutput(bestFitnessValue, cv);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}

	}
}
